package org.colswe.junitlab.logica.imp;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.colswe.junitlab.logica.IControlFestivo;
import org.colswe.junitlab.logica.exception.DatosInvalidosException;
import org.colswe.junitlab.modelo.Festivo;
import org.colswe.junitlab.modelo.Sistema;
import org.colswe.junitlab.modelo.TipoDia;

/**
* Esta clase es la encargada del control de festivos
*/
public class ControlFestivo implements IControlFestivo {
	
	// Constantes utilizadas en el sistema para definir la cantidad de horas labrables por tipo de día
	//Horas correspondientes a un festivo laboral
	public static final int HORIAS_FESTIVO_LABORAL =16; 
	// Horas correspondientes a un festivo no laboral
	public static final int HORAS_FESTIVO_NO_LABORAL =0;
	//Horas correspondientes a un sabado
	public static final int HORAS_SABADO = 4; 
	//Horas correspondientes a un día normal
	public static final int HORAS_DIA_NORMAL = 8; 

	
	private Sistema sistema = Sistema.getInstance();

	/**
	 * Retorna la cantidad de horas que restan por trabajar en un dia laboral
	 * 
	 * @param Desde
	 *            fecha para revisar las horas laborales
	 */

	public Integer contarHorasPorTrabajar(Date desde) throws DatosInvalidosException {
		// fecha con la hora ingresada por usuario
		Calendar desdeCalendar = Calendar.getInstance();
		// fecha con la hora de entrada del día
		Calendar entrada= Calendar.getInstance();
		// fecha con la hora de salida del día
		Calendar salida= Calendar.getInstance();
		// tipología del día
		TipoDia tipo = null;	
		// si se trata o no de un día festivo
		boolean isFestivo=false;
	
		desdeCalendar.setLenient(false);
		// comprobaciones de válidez en la fecha ingresada
		try{
			desdeCalendar.setTime(desde);
			entrada.setTime(desde);
			salida.setTime(desde);	
		}
		catch(Exception e){
			throw new DatosInvalidosException();
		}	
		// comprobación de si el día es o no festivo
		for(Festivo f: sistema.getEntidades()){
			if(esMismoDia(desde,f.getFecha())){
				isFestivo=true;
				if(f.isLaborable()){
					tipo=TipoDia.FESTIVO_LABORA;
				}
				else{
					tipo=TipoDia.FESTIVO_NO_LABORAL;
				}
			}
		}		
		if(!isFestivo){
			if(desdeCalendar.get(Calendar.DAY_OF_WEEK)==Calendar.SATURDAY){
				tipo=TipoDia.SABADO;
			}
			else{
				// si no se trata de un domingo entonces es un día normal
				if(desdeCalendar.get(Calendar.DAY_OF_WEEK)==Calendar.SUNDAY){
					tipo=TipoDia.FESTIVO_NO_LABORAL;
				}
				else{
					tipo=TipoDia.NORMAL;
				}
			}
		}
		// definición de horas de ingreso y salida según tipología
		switch(tipo){
		case NORMAL:
			entrada.set(Calendar.HOUR_OF_DAY, 8);
			entrada.set(Calendar.MINUTE, 0);
			salida.set(Calendar.HOUR_OF_DAY, 16);
			salida.set(Calendar.MINUTE, 0);
			break;
		case FESTIVO_LABORA:
			entrada.set(Calendar.HOUR_OF_DAY, 8);
			entrada.set(Calendar.MINUTE, 0);
			salida.set(Calendar.HOUR_OF_DAY, 16);
			salida.set(Calendar.MINUTE, 0);
			break;
		case SABADO:
			entrada.set(Calendar.HOUR_OF_DAY, 8);
			entrada.set(Calendar.MINUTE, 0);
			salida.set(Calendar.HOUR_OF_DAY, 12);
			salida.set(Calendar.MINUTE, 0);
			break;
		case FESTIVO_NO_LABORAL:
			break;		
		default:			
			throw new DatosInvalidosException();		
		}
		// truncado al intervalo
		if(desdeCalendar.before(entrada)){
			desdeCalendar=entrada;
		}
		if(desdeCalendar.after(salida)){
			desdeCalendar=salida;			
		}	
		// horas Transcurridas desde la entrada
		int horasT= desdeCalendar.get(Calendar.HOUR_OF_DAY);
		if(desdeCalendar.get(Calendar.MINUTE)>0){
			horasT++;
		}
		// horas laborales resultantes
		int horasL=salida.get(Calendar.HOUR_OF_DAY) - horasT;
		// transformación si se trata de un festivo
		if(isFestivo){
			horasL*=2;
		}
		
		return horasL;
	}
	
	/**
	 * Función para comprobar si dos fechas son iguales sin contar
	 * la hora en que se encuentran
	 * @param Date esta
	 * @param Date otra
	 * @return true - si esta==otra sin tener en cuenta hora
	 * 		   false - otherwise 
	 */
	public boolean esMismoDia(Date esta, Date otra){
		Calendar cEsta= Calendar.getInstance();
		Calendar cOtra= Calendar.getInstance();
		cEsta.setTime(esta);
		cOtra.setTime(otra);
		return cEsta.get(Calendar.YEAR)==cOtra.get(Calendar.YEAR)
				&& cEsta.get(Calendar.DAY_OF_YEAR) == cOtra.get(Calendar.DAY_OF_YEAR);
	}
	/**
	 * Retorna la suma entre dos intervalos definidos por
	 * 
	 * @param fecha
	 *            inicial del primer intervalo
	 * @param fecha
	 *            final del primer intervalo
	 * @param fecha
	 *            inicial del segundo intervalo
	 * @param fecha
	 *            final del segundo intervalo
	 */
	public Integer contarHoras(Date desdeI, Date hastaI, Date desdeII,
			Date hastaII) throws DatosInvalidosException {		
		//Se debe calcular la cantidad de horas en dos intervalos
		int horasLaborables=0;
		//Se validan las fechas
		if (fechasValidas(desdeI, hastaI) && fechasValidas(desdeII, hastaII)) {
			// los intervalos se encuentran solapados
			if ((desdeII.before(hastaI) && desdeI.before(desdeII))
					|| (desdeI.before(hastaII) && desdeII.before(desdeI))) {
				//Se lanza la excepción
				throw new DatosInvalidosException();
			}
			//Se usan el metodo contar horas para sacar cada intervalo ultimamente
			horasLaborables=contarHoras(desdeI, hastaI) + contarHoras(desdeII, hastaII);
		}
		//Se retorna las horas laborables
		return horasLaborables;
	}
	
	
	/**
	 * Retorna el numero de horas existentes entre dos fechas dadas
	 * 
	 * @param desde
	 *            dia inicial 
	 * @param hasta
	 *            dia final 
	 * @return numero de horas           
	 */
	public Integer contarHoras(Date desde, Date hasta)
			throws DatosInvalidosException {
		//Se obtiene el mapa desde el método obtenerDias
		Map<TipoDia, Integer> dias = obtenerDias(desde, hasta);
		//se retorna el mapa
		return contarHoras(dias);
	}

	/**
	 * Retorna un entero que contiene la cantidad de horas existentes en un mapa
	 * 
	 * @param Map<TipoDia, Integer> 
	 *             info Mapa que contiene los diferentes dias y sus respectivos valores en horas 
	 * @return numero de horas           
	 */
	public Integer contarHoras(Map<TipoDia, Integer> info)
			throws DatosInvalidosException {
		//Se inicializan las horas en cero
		int horas = 0;
		//Se crea una llave para para el mapa
		TipoDia key;
		Integer value;
		//Se crea el iterador para el mapa
		Iterator<TipoDia> iterator = info.keySet().iterator();
		//Se recorre el iterador
		while (iterator.hasNext()) {
			key = iterator.next();
			value = info.get(key);
			if (value < 0) {
				throw new DatosInvalidosException();
			}
		}
		//Se agrega las horas del festivo laborarl
		horas += info.get(TipoDia.FESTIVO_LABORA) * HORIAS_FESTIVO_LABORAL;
		//Se agreaga el festivo no laboral
		horas += info.get(TipoDia.FESTIVO_NO_LABORAL) *HORAS_FESTIVO_NO_LABORAL;
		//Se agregan las horas del sabado 
		horas += info.get(TipoDia.SABADO) * HORAS_SABADO;
		//Se agregan las horas del día normal
		horas += info.get(TipoDia.NORMAL) * HORAS_DIA_NORMAL;
		//Se retorna el numero total de horas
		return horas;
	}

	/**
	 * Retorna verdadero si la fecha dada es valida o falso en el caso contrario
	 * 
	 * @param desde
	 *            dia inicial 
	 * @param hasta
	 *            dia final 
	 */
	public boolean fechasValidas(Date desde, Date hasta) {
		//Se valida que las fechas no sean nulas
		if (desde == null || hasta == null) {
			//Se retorna false si son nulas
			return false;
		}
		//Se valida que la fecha desde sea menor que la fecha hasta
		return desde.before(hasta);
	}

	/**
	 * Retorna el mapa con los dias separados por tipo
	 * 
	 * @param desde
	 *            dia inicial inclusive
	 * @param hasta
	 *            dia final inclusive
	 */
	public Map<TipoDia, Integer> obtenerDias(Date desde, Date hasta) {
		HashMap<TipoDia, Integer> ret = new HashMap<TipoDia, Integer>();
		//Se inicializa el mapa en dia laboral
		ret.put(TipoDia.FESTIVO_LABORA, 0);
		//Se inicializa el mapa en dia no laboral
		ret.put(TipoDia.FESTIVO_NO_LABORAL, 0);
		//Se inicializa el mapa en dia normal
		ret.put(TipoDia.NORMAL, 0);
		//Se inicializa el mapa en dia sabado
		ret.put(TipoDia.SABADO, 0);
		//Se validan las fechas
		if (!fechasValidas(desde, hasta)) {
			//se retorna el mapa vacio
			return ret;
		}
		Calendar cH = Calendar.getInstance();
		//Se crea el calendar con la fehca de hasta
		cH.setTime(hasta);
		//Se agrega un día al hasta 
		cH.add(Calendar.DATE, 1);
		Calendar c = Calendar.getInstance();
		c.setTimeInMillis(desde.getTime());
		//Se inicializa la cuenta de cada tipo de dia en cero
		int sabado = 0;
		int festivoL = 0;
		int festivoNL = 0;
		int normal = 0;
		
		while (cH.after(c)) {
			
			boolean festivo = false;
			for (Festivo f : sistema.getEntidades()) {
				
				if (f.getFecha().equals(c.getTime())) {
					
					festivo = true;
					if (f.isLaborable()) {
						festivoL++;
					} else {
						Calendar ct = Calendar.getInstance();
						ct.setTime(f.getFecha());
						festivoNL += calcularCantidadDeFestivosNoLaborables(ct);
					}
				}
			}
			if (pregunta(festivo, c, Calendar.SATURDAY, true)) {
				sabado++;
			} else if (pregunta(festivo, c, Calendar.SUNDAY, false)) {
				normal++;
			}
			c.add(Calendar.DATE, 1);
		}
		ret.clear();
		//Coloca en el mapa el festivo laboral
		ret.put(TipoDia.FESTIVO_LABORA, festivoL);
		//Coloca en el mapa el festivo no laboral
		ret.put(TipoDia.FESTIVO_NO_LABORAL, festivoNL);
		//Coloca en el mapa el dia normal
		ret.put(TipoDia.NORMAL, normal);
		//Coloca en mapa el sabado
		ret.put(TipoDia.SABADO, sabado);

		return ret;
	}


	/**
	 * @return the sistema
	 */
	public Sistema getSistema() {
		//retorna el sistema
		return sistema;
	}


	/**
	 * @param sistema the sistema to set
	 */
	public void setSistema(Sistema sistema) {
		//setea el sistema
		this.sistema = sistema;
	}


	/**
	 * Se crean estos métodos para reducir complejidad ciclomatica
	 * @param esFestivo para saber si es festivo
	 * @param c Calendar
	 * @param dia Saber si es sabado o domingo
	 * @param esIgual 
	 * */
	private boolean pregunta(boolean esFestivo, Calendar c, int dia, boolean esIgual){
		 //Se pregunta se se va a validas por una igualdad
		 if(esIgual){
			//Se hace la pregunta 
		 	return !esFestivo && c.get(Calendar.DAY_OF_WEEK) == dia;
		 }
		 //Sino se pregunta por si es diferente
		 else{
			//Se hace la pregunta 
		 	return !esFestivo && c.get(Calendar.DAY_OF_WEEK) != dia;
		 	} 
	}
	
	/**
	 * Se calcula la cantidad de festivos con una pregunta
	 * */
	private int calcularCantidadDeFestivosNoLaborables(Calendar c){
		 //Se hace la pregunta segun los parámetros
		 return pregunta(false, c, Calendar.SUNDAY, true) ? 0 : 1;
    }

}
